package com.denlaku.longan.audit;

import com.denlaku.longan.anno.Audit;
import com.denlaku.longan.core.Current;
import com.denlaku.longan.service.AuditLogService;
import com.denlaku.longan.util.SnowflakeUtils;
import com.denlaku.longan.util.ThreadPoolUtil;
import com.denlaku.longan.vo.AuditLog;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Controller切面
 *
 * @author tianx
 */
@Aspect
@Slf4j
@Component
public class AuditAspect {
	private final ParameterNameDiscoverer parameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();

	private final ConcurrentMap<String, Expression> expressionMap = new ConcurrentHashMap<>();

	private final ConcurrentMap<Method, String[]> parameterNamesMap = new ConcurrentHashMap<>();

	@Autowired
	private AuditLogService auditLogService;

	/**
	 * 环绕通知：
	 * 当Controller层抛出异常，保证接口返回数据结构保持一致
	 *
	 * @param proceeding proceeding
	 * @return 返回值
	 */
	@Around("@annotation(audit)")
	@SneakyThrows
	public Object around(ProceedingJoinPoint proceeding, Audit audit) {
		try {
			AuditVariables.init();
			return proceeding.proceed();
		} finally {
			addAuditLog(proceeding, audit);
			AuditVariables.clear();
		}
	}

	private void addAuditLog(ProceedingJoinPoint proceeding, Audit audit) {
		try {
			Map<String, Object> variables = AuditVariables.getVariables();
			ThreadPoolUtil.execAsync(() -> {
				AuditLog auditLog = initAuditLog(proceeding, audit, variables);
				log.info(auditLog.getMessage());
				auditLogService.add(auditLog);
			});
		} catch (Exception e) {
			log.error("Failed to add audit log", e);
		}
	}

	private AuditLog initAuditLog(ProceedingJoinPoint proceeding, Audit audit, Map<String, Object> variables) {
		AuditLog auditLog = new AuditLog();
		auditLog.setId(SnowflakeUtils.nextId());
		auditLog.setTenantId(ObjectUtils.getIfNull(Current.getTenantId(), -1L));
		auditLog.setMessage(parseMessage(proceeding, audit, variables));
		auditLog.setModule(audit.module());
		auditLog.setOperation(audit.operation());
		auditLog.setCreatedBy(Current.getUserId());
		return auditLog;
	}

	private String parseMessage(ProceedingJoinPoint proceeding, Audit audit, Map<String, Object> variables) {
		String template = audit.message();
		Expression expression = getExpression(template);
		EvaluationContext evaluationContext = initEvaluationContext(proceeding, variables);
		return expression.getValue(evaluationContext, String.class);
	}

	private EvaluationContext initEvaluationContext(ProceedingJoinPoint proceeding, Map<String, Object> variables) {
		Object[] args = proceeding.getArgs();
		EvaluationContext evaluationContext = new StandardEvaluationContext();
		for (int i = 0; i < args.length; i++) {
			evaluationContext.setVariable("p" + i, args[i]);
		}
		String[] parameterNames = getParameterNames(proceeding);
		for (int i = 0; i < parameterNames.length; i++) {
			evaluationContext.setVariable(parameterNames[i], args[i]);
		}
		variables.forEach(evaluationContext::setVariable);
		return evaluationContext;
	}

	private Expression getExpression(String expressionString) {
		return expressionMap.computeIfAbsent(expressionString, key -> {
			ParserContext parserContext = new TemplateParserContext("{", "}");
			ExpressionParser expressionParser = new SpelExpressionParser();
			return expressionParser.parseExpression(expressionString, parserContext);
		});
	}

	private String[] getParameterNames(ProceedingJoinPoint proceeding) {
		Object[] args = proceeding.getArgs();
		Signature signature = proceeding.getSignature();
		String name = signature.getName();
		Class<?> declaringType = signature.getDeclaringType();
		for (Method method : declaringType.getMethods()) {
			int parameterCount = method.getParameterCount();
			if (args.length == parameterCount && method.getName().equals(name)) {
				return parameterNamesMap.computeIfAbsent(method,
						key -> parameterNameDiscoverer.getParameterNames(method));
			}
		}
		return new String[0];
	}


}
